exports.parse函数源码:#L239
这个parse函数样例入参:
parse("a.b.['c'].d")
最后返回值是一个数组(带有一个get函数的数组): ["a", "b", "c", "d"]
源码:
exports.parse = function (path) {
var hit = pathCache.get(path)
if (!hit) {
hit = parsePath(path)
if (hit) {
hit.get = exports.compileGetter(hit)
pathCache.put(path, hit)
}
}
return hit
}
parsePath关键代码,返回值是一个数组
parsePath github:L140
源码:
function parsePath (path) {
var keys = []
var index = -1
var mode = 'beforePath'
var c, newChar, key, type, transition, action, typeMap
var actions = {
push: function() {
if (key === undefined) {
return
}
keys.push(key)
key = undefined
},
append: function() {
if (key === undefined) {
key = newChar
} else {
key += newChar
}
}
}
function maybeUnescapeQuote () {
var nextChar = path[index + 1]
if ((mode === 'inSingleQuote' && nextChar === "'") ||
(mode === 'inDoubleQuote' && nextChar === '"')) {
index++
newChar = nextChar
actions.append()
return true
}
}
while (mode) {
index++
c = path[index]
if (c === '\\' && maybeUnescapeQuote()) {
continue
}
type = getPathCharType(c)
typeMap = pathStateMachine[mode]
transition = typeMap[type] || typeMap['else'] || 'error'
if (transition === 'error') {
return // parse error
}
mode = transition[0]
action = actions[transition[1]] || noop
newChar = transition[2] === undefined
? c
: transition[2]
action()
if (mode === 'afterPath') {
return keys
}
}
}
核心逻辑是在 github:L10的状态机变量pathStateMachine。
通过一个while循环,来执行掉这个pathStateMachine状态机。
每一个pathStateMachine的子属性里头的孙子属性,都是一个数组。最大是3位的数组。index从0到2,分别代表"下一步mode,也称状态", "action", "替换原先key的字符串"。
其中action在github:L146指定了两种,push和append。push类似于在最后parse出来的结果数组(暂称为rst)里头,增加一个key;append是目前这个key增加字符。 举例说明,假设目前rst已经是["abc", "d"],目前action是append,然后当前字符是e,也即将e添加到rst[1]的字符串后面行程["abc", "de"];如果当前action是push,那么就是["abc", "d", "e"]
index为0的——“下一步mode,也称状态”,这个是通过getPathCharType这个函数+前面提到的孙子属性的数组,结合得到下一步的“状态”。
譬如pathStateMachine.beforePath,其原始值如下:
{ 'beforePath': {
'ws': ['beforePath'],
'ident': ['inIdent', 'append'],
'[': ['beforeElement'],
'eof': ['afterPath']
}}
parsePath的状态机pathStateMatchine遍历的举例说明,辅助理解。
举个例子:解析 "user.addresses[0]['street']" 的过程简化如下:
| 字符 | 状态转移 | 动作 | keys |
|---|---|---|---|
u |
beforePath → inIdent | append | key="u" |
s e r |
inIdent → inIdent | append | key="user" |
. |
inIdent → beforeIdent | push | ["user"] |
a..s |
beforeIdent → inIdent × n | append | key="addresses" |
[ |
inIdent → beforeElement | push | ["user","addresses"] |
0 |
beforeElement → afterZero | append | key="0" |
] |
afterZero → inPath | push | ["user","addresses","0"] |
[ |
inPath → beforeElement | - | |
' |
beforeElement → inSingleQuote | append '' | key="" |
s..t |
inSingleQuote → inSingleQuote | append | key="street" |
' |
inSingleQuote → afterElement | - | (不把引号加进去) |
] |
afterElement → inPath | push | ["user","addresses","0","street"] |
| EOF | inPath → afterPath | - | 返回结果 ✅ |
hit.get是一个compileGetter返回的函数。
为何给返回值的这个数组(hit)增加这么一个get函数
能快速方便使用,从而从一个入参object里头获取到对应的属性值(多层path)。 例如:
var parsed = path. parse ( 'user.name.first' )
var obj = { user : { name : { first : 'Tom' } } }
parsed. get (obj) // → 'Tom'
exports.get函数源码:#L258
使用了exports.parse里头的hit.get这个挂载的函数,再封装一层就是exports.get函数
exports.get = function (obj, path) {
path = exports.parse(path)
if (path) {
return path.get(obj)
}
}
exports.set函数源码:#L273
exports.set = function (obj, path, val) {
if (typeof path === 'string') {
path = exports.parse(path)
}
if (!path || !_.isObject(obj)) {
return false
}
var last, key
for (var i = 0, l = path.length - 1; i < l; i++) {
last = obj
key = path[i]
obj = obj[key]
if (!_.isObject(obj)) {
obj = {}
last.$add(key, obj)
}
}
key = path[i]
if (key in obj) {
obj[key] = val
} else {
obj.$add(key, val)
}
return true
}
核心是用了x.$add(xx, xx)函数,将属性添加上去,并有响应式
需要注意L290行的关键代码key = path[i],取得就是最后一个key。
仔细看L281行的for循环,一开始l赋值的就是length - 1,然后后面是i < l这个判断,所以for循环是会跑到倒数第二个key之后就结束,从而 L290行那一行取到的是最后一个key。
一般上的for循环会赋值l = length,然后在i < l。但L281行那里却是l = length - 1
exports.parse为何不能直接eval,而要先parsePath,然后再compileGetter获取?
- 代码注入风险。通过parsePath再compileGetter合起来,实现了白名单式代码。
- parsePath之后还能给别的用。譬如
set相关、watch相关等等。如果只有eval,你无法拿到['user', 'address', 'city']这样的分段信息。 parsePath的状态机会对路径做完整的语法校验:
- 引号不闭合 → 返回
undefined(如'a["b') - 非法字符 → 返回
undefined(如'a@b') - 数字索引格式错误 → 返回
undefined - 而
eval遇到错误会 抛出异常 ,调用方必须 try-catch,而且错误信息是 JS 引擎的原始报错,不友好。
if (+key === key >>> 0)这段代码小技巧。
属于快速判断这个key是>=0的正整数。 在这个具体逻辑里,是用来判断是合法的数组下标值。
关键原理是>>>这个操作。类似还有个>>操作
>>> 是 JS 无符号右移运算符(zero-fill right shift)。
移位运算在 JS 里有一条硬性规则:
运算前,先把操作数转为 32 位有符号整数(ToInt32),再执行移位;无符号右移的结果类型是 32 位无符号整数。
>>> 0 就是不移位,只利用这个类型转换机制。
转换流程拆解:
- 先对
key执行抽象操作ToNumber(key)→ 得到数字 - 再执行
ToInt32:把数字截断为32 位有符号二进制补码- 范围:
[-2³¹, 2³¹−1],也就是-2147483648 ~ 2147483647 - 小数直接舍弃小数部分;超出范围则只保留低 32bit;
NaN/Infinity变成 0
- 范围:
- 然后执行右移 0 位:比特不变
- 但
>>>是无符号移位:输出时,把这 32 位比特解释成无符号数- 最高位不再是符号位,而是普通数值位
- 结果范围:
[0, 4294967295](2³² −1)
>>操作
有符号右移 0 位,转成 32 位有符号整数,结果可负,范围 -2147483648 ~ 2147483647
(-1) >> 0 // -1
(-1) >>> 0 // 4294967295